﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace NMapper.Utils
{
    public static class Extensions
    {
        public static IEnumerable<PropertyInfo> GetPropertyPath(this LambdaExpression expression)
        {
            MemberExpression member = expression.Body as MemberExpression;
            if (member == null) 
                return Enumerable.Empty<PropertyInfo>();
            return member
                .SelectRecursive(o => o.Expression is MemberExpression ? (MemberExpression)o.Expression : null)
                .Select(o => (PropertyInfo)o.Member).Reverse();
        }         

        public static bool SetProperty(this LambdaExpression expression, object target, object value)
        {
            var path = expression.GetPropertyPath();
            var parentPath = path.Take(path.Count() - 1);
            var property = path.LastOrDefault();
            if (property == null)
                return false;

            var current = target;
            foreach (var item in parentPath)
            {
                var parent = current;
                current = item.GetValue(current, null);
                if (current == null)
                {
                    current = Activator.CreateInstance(item.PropertyType);
                    item.SetValue(parent, current, null);
                }
            }
            property.SetValue(current, value, null);
            return true;
        }

        public static IEnumerable<T> SelectRecursive<T>(this T obj, Func<T, T> next) where T : class
        {
            T current = obj;
            while (current != null)
            {
                yield return current;
                current = next(current);
            }
        }

        public static void LoadInstanceField(this ILGenerator il, FieldInfo field)
        {
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, field);            
        }

        public static void StoreValueInInstanceField(this ILGenerator il, FieldInfo field)
        {
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Stfld, field);            
        }

        public static PropertyBuilder DefineProperty(this TypeBuilder type, string propertyName, Type propertyType)
        {
            return DefineProperty(type, propertyName, propertyType,
                (il, field) => il.LoadInstanceField(field),
                (il, field) => il.StoreValueInInstanceField(field));
        }

        public static PropertyBuilder DefineProperty(this TypeBuilder type, string propertyName, Type propertyType, Action<ILGenerator, FieldBuilder> getter, Action<ILGenerator, FieldBuilder> setter)
        {
            FieldBuilder field = type.DefineField(propertyName.Decapitalize(), propertyType, FieldAttributes.Private);
            return DefineProperty(type, propertyName, propertyType, getter, setter, field);
        }

        public static PropertyBuilder DefineProperty(this TypeBuilder type, string propertyName, Type propertyType, Action<ILGenerator, FieldBuilder> getter, Action<ILGenerator, FieldBuilder> setter, FieldBuilder field)
        {
            MethodBuilder getMethod = type.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual, propertyType, Type.EmptyTypes);
            {
                ILGenerator il = getMethod.GetILGenerator();
                getter(il, field);
                il.Emit(OpCodes.Ret);
            }

            MethodBuilder setMethod = type.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual, typeof(void), new[] { propertyType });
            {
                ILGenerator il = setMethod.GetILGenerator();
                setter(il, field);
                il.Emit(OpCodes.Ret);
            }

            PropertyBuilder property = type.DefineProperty(propertyName, PropertyAttributes.None, propertyType, Type.EmptyTypes);
            property.SetGetMethod(getMethod);
            property.SetSetMethod(setMethod);
            return property;            
        }

        public static PropertyInfo GetPropertyInfo(this LambdaExpression expression)
        {
            var current = expression.Body;
            var unary = current as UnaryExpression;
            if (unary != null)
                current = unary.Operand;
            var call = current as MemberExpression;
            if (call == null)
                return null;
            return call.Member as PropertyInfo;
        }

        public static MethodInfo GetGenericMethod(this Type type, string methodName, Type[] typeArguments, Type[] parameterTypes)
        {
            MemberInfo[] members = type.FindMembers(
                MemberTypes.Method,
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static,
                (m, criteria) =>
                {
                    MethodInfo methodInfo = m as MethodInfo;
                    if (m != null && 
                        m.Name == methodName &&
                        methodInfo.IsGenericMethodDefinition &&
                        methodInfo.GetGenericArguments().Length == typeArguments.Length &&
                        methodInfo.GetParameters().Length == parameterTypes.Length)
                    {
                        methodInfo = methodInfo.MakeGenericMethod(typeArguments);
                        ParameterInfo[] methodParameterTypes = methodInfo.GetParameters();
                        int index = 0;
                        foreach (ParameterInfo methodParameter in methodParameterTypes)
                        {
                            if (!methodParameter.ParameterType.IsAssignableFrom(parameterTypes[index++]))
                                return false;
                        }
                        return true;
                    }
                    return false;
                },
                null);

            if (members.Length == 0)
                return null;
            if (members.Length > 1)
                throw new InvalidOperationException();

            MethodInfo method = (MethodInfo)members[0];
            method = method.MakeGenericMethod(typeArguments);

            return method;
        }

        public static bool IsGenericList(this Type listType)
        {
            return IsType(listType, typeof(List<>)) || IsType(listType, typeof(Collection<>)) || IsType(listType, typeof(IList<>));
        }

        public static bool IsType(Type type, Type ancestor)
        {
            while (type != null)
            {
                if (type.IsGenericType)
                    type = type.GetGenericTypeDefinition();
                if (type == ancestor)
                    return true;
                type = type.BaseType;
            }
            return false;
        }

        public static string Decapitalize(this string s)
        {
            return Decapitalize(s, false);
        }

        public static string Decapitalize(this string s, bool correctCase)
        {
            StringBuilder builder = new StringBuilder();
            if (s.Length > 0)
                builder.Append(char.ToLower(s[0]));
            for (int i = 1; i < s.Length; i++)
            {
                builder.Append(correctCase ? char.ToLower(s[i]) : s[i]);
            }
            return builder.ToString();
        }

        public static bool IsNullableType(this Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        /// <summary>
        /// Compares `source` with `mergeWith`.  Items that are contained in `mergeWith` that are not contained in `source` 
        /// are placed in the `Added` property on the return value.  Items that are contained in `source` but not contained
        /// in `mergeWith` are placed in the `Removed` property on the return value.
        /// </summary>
        public static MergeResult<T> Merge<T>(this IEnumerable<T> source, IEnumerable<T> mergeWith)
        {
            var added = new List<T>();
            var removed = new List<T>();
            var inBoth = new List<T>();

            var sourceSet = new HashSet<T>(source);
            var mergeWithSet = new HashSet<T>(mergeWith);

            foreach (var item in sourceSet)
            {
                if (!mergeWithSet.Contains(item))
                    removed.Add(item);
                else
                    inBoth.Add(item);
            }
            foreach (var item in mergeWithSet)
                if (!sourceSet.Contains(item))
                    added.Add(item);

            return new MergeResult<T>(added, removed, inBoth);
        }

        public struct MergeResult<T>
        {
            private readonly List<T> added;
            private readonly List<T> removed;
            private readonly List<T> inBoth;

            public MergeResult(List<T> added, List<T> removed, List<T> inBoth) : this()
            {
                this.added = added;
                this.removed = removed;
                this.inBoth = inBoth;
            }

            public List<T> Added
            {
                get { return added; }
            }

            public List<T> Removed
            {
                get { return removed; }
            }

            public List<T> InBoth
            {
                get { return inBoth; }
            }
        }
    }
}